using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Collections;
using System.Security.Cryptography;
using qip_history_synchronizer.Core;
using qip_history_synchronizer.ServersCore;

namespace qip_history_synchronizer.Pop3
{
    /// <summary>
    /// POP3 Client.
    /// </summary>
    /// <example>
    /// <code>
    /// 
    /// /*
    ///  To make this code to work, you need to import following namespaces:
    ///  using LumiSoft.Net.Mime;
    ///  using LumiSoft.Net.POP3.Client; 
    ///  */
    /// 
    /// using(POP3_Client c = new POP3_Client()){
    ///		c.Connect("ivx",110);
    ///		c.Authenticate("test","test",true);
    ///		
    ///		POP3_MessagesInfo mInf = c.GetMessagesInfo();
    ///		
    ///		// Get first message if there is any
    ///		if(mInf.Count > 0){
    ///			byte[] messageData = c.GetMessage(mInf[0]);
    ///		
    ///			// Do your suff
    ///			
    ///			// Parse message
    ///			Mime m = Mime.Parse(messageData);
    ///			string from = m.MainEntity.From;
    ///			string subject = m.MainEntity.Subject;			
    ///			// ... 
    ///		}		
    ///	}
    /// </code>
    /// </example>
    public class POP3_Client : IDisposable
    {
        private SocketEx     m_pSocket       = null;
        private SocketLogger m_pLogger       = null;
        private bool         m_Connected     = false;
        private bool         m_Authenticated = false;
        private string       m_ApopHashKey   = "";
        private bool         m_LogCmds       = false;

        /// <summary>
        /// Occurs when POP3 session has finished and session log is available.
        /// </summary>
        public event LogEventHandler SessionLog = null;

        /// <summary>
        /// Default constructor.
        /// </summary>
        public POP3_Client()
        {				
        }

        #region method Dispose

        /// <summary>
        /// Clean up any resources being used.
        /// </summary>
        public void Dispose()
        {
            try{
                Disconnect();
            }
            catch{
            }
        }

        #endregion


        #region method Connect

        /// <summary>
        /// Connects to specified host.
        /// </summary>
        /// <param name="host">Host name.</param>
        /// <param name="port">Port number. Default POP3 port is 110.</param>
        public void Connect(string host,int port)
        {
            Connect(host,port,false);
        }

        /// <summary>
        /// Connects to specified host.
        /// </summary>
        /// <param name="host">Host name.</param>
        /// <param name="port">Port number. Default POP3 port is 110 and SSL port is 995.</param>
        /// <param name="ssl">Specifies if to connected via SSL.</param>
        public void Connect(string host,int port,bool ssl)
        {
            if(!m_Connected){
                SocketEx s = new SocketEx();
                s.Connect(host,port,ssl);
                m_pSocket = s;

                if(m_LogCmds && SessionLog != null){
                    m_pLogger = new SocketLogger(s.RawSocket,SessionLog);
                    m_pLogger.SessionID = Guid.NewGuid().ToString();
                    m_pSocket.Logger = m_pLogger;
                }

                // Set connected flag
                m_Connected = true;

                string reply = m_pSocket.ReadLine();
                if(reply.StartsWith("+OK")){
                    // Try to read APOP hash key, if supports APOP
                    if(reply.IndexOf("<") > -1 && reply.IndexOf(">") > -1){
                        m_ApopHashKey = reply.Substring(reply.LastIndexOf("<"),reply.LastIndexOf(">") - reply.LastIndexOf("<") + 1);
                    }
                }
            }
        }

        #endregion

        #region method Disconnect

        /// <summary>
        /// Closes connection to POP3 server.
        /// </summary>
        public void Disconnect()
        {
            try{
                if(m_pSocket != null){
                    // Send QUIT
                    m_pSocket.WriteLine("QUIT");			

                    m_pSocket.Shutdown(SocketShutdown.Both);					
                }
            }
            catch{
            }

            if(m_pLogger != null){
                m_pLogger.Flush();
            }
            m_pLogger = null;

            m_pSocket       = null;
            m_Connected     = false;			
            m_Authenticated = false;
        }

        #endregion

        #region method StartTLS

        /// <summary>
        /// Switches POP3 connection to SSL.
        /// </summary>
        public void StartTLS()
        {
            /* RFC 2595 4. POP3 STARTTLS extension.
                Arguments: none

                Restrictions:
                    Only permitted in AUTHORIZATION state.
             
                Possible Responses:
                     +OK -ERR

                 Examples:
                     C: STLS
                     S: +OK Begin TLS negotiation
                     <TLS negotiation, further commands are under TLS layer>
                       ...
                     C: STLS
                     S: -ERR Command not permitted when TLS active
            */

            if(!m_Connected){
                throw new Exception("You must connect first !");
            }
            if(m_Authenticated){
                throw new Exception("The STLS command is only valid in non-authenticated state !");
            }
            if(m_pSocket.SSL){
                throw new Exception("Connection is already secure !");
            }

            m_pSocket.WriteLine("STLS");

            string reply = m_pSocket.ReadLine();
            if(!reply.ToUpper().StartsWith("+OK")){
                throw new Exception("Server returned:" + reply);
            }

            m_pSocket.SwitchToSSL_AsClient();
        }

        #endregion

        #region method Authenticate

        /// <summary>
        /// Authenticates user.
        /// </summary>
        /// <param name="userName">User login name.</param>
        /// <param name="password">Password.</param>
        /// <param name="tryApop"> If true and POP3 server supports APOP, then APOP is used, otherwise normal login used.</param>
        public void Authenticate(string userName,string password,bool tryApop)
        {
            if(!m_Connected){
                throw new Exception("You must connect first !");
            }

            if(m_Authenticated){
                throw new Exception("You are already authenticated !");
            }

            // Supports APOP, use it
            if(tryApop && m_ApopHashKey.Length > 0){
                //--- Compute md5 hash -----------------------------------------------//
                byte[] data = System.Text.Encoding.ASCII.GetBytes(m_ApopHashKey + password);
			
                MD5 md5 = new MD5CryptoServiceProvider();			
                byte[] hash = md5.ComputeHash(data);

                string hexHash = BitConverter.ToString(hash).ToLower().Replace("-","");
                //---------------------------------------------------------------------//

                m_pSocket.WriteLine("APOP " + userName + " " + hexHash);

                string reply = m_pSocket.ReadLine();
                if(reply.StartsWith("+OK")){
                    m_Authenticated = true;
                }
                else{
                    throw new Exception("Server returned:" + reply);
                }
            }
            else{ // Use normal LOGIN, don't support APOP 
                m_pSocket.WriteLine("USER " + userName);

                string reply = m_pSocket.ReadLine();
                if(reply.StartsWith("+OK")){
                    m_pSocket.WriteLine("PASS " + password);

                    reply = m_pSocket.ReadLine();
                    if(reply.StartsWith("+OK")){
                        m_Authenticated = true;
                    }
                    else{
                        throw new Exception("Server returned:" + reply);
                    }
                }
                else{
                    throw new Exception("Server returned:" + reply);
                }				
            }

            if(m_Authenticated && m_pSocket.Logger != null){
                m_pSocket.Logger.UserName = userName;
            }
        }

        #endregion


        #region function GetMessagesInfo

        /// <summary>
        /// Gets messages info.
        /// </summary>
        public POP3_MessagesInfo GetMessagesInfo()
        {
            if(!m_Connected){
                throw new Exception("You must connect first !");
            }

            if(!m_Authenticated){
                throw new Exception("You must authenticate first !");
            }

            POP3_MessagesInfo messagesInfo = new POP3_MessagesInfo();

            // Before getting list get UIDL list, then we can make full message info (UID,No,Size).
            Hashtable uidlList = GetUidlList();

            m_pSocket.WriteLine("LIST");

            /* NOTE: If reply is +OK, this is multiline respone and is terminated with '.'.
			Examples:
				C: LIST
				S: +OK 2 messages (320 octets)
				S: 1 120				
				S: 2 200
				S: .
				...
				C: LIST 3
				S: -ERR no such message, only 2 messages in maildrop
			*/

            // Read first line of reply, check if it's ok
            string line = m_pSocket.ReadLine();
            if(line.StartsWith("+OK")){
                // Read lines while get only '.' on line itshelf.
                while(true){
                    line = m_pSocket.ReadLine();

                    // End of data
                    if(line.Trim() == "."){
                        break;
                    }
                    else{
                        string[] param = line.Trim().Split(new char[]{' '});
                        int  no   = Convert.ToInt32(param[0]);
                        long size = Convert.ToInt64(param[1]);

                        messagesInfo.Add(uidlList[no].ToString(),no,size);
                    }
                }
            }
            else{
                throw new Exception("Server returned:" + line);
            }

            return messagesInfo;
        }

        #endregion

        #region function GetUidlList

        /// <summary>
        /// Gets uid listing.
        /// </summary>
        /// <returns>Returns Hashtable containing uidl listing. Key column contains message NR and value contains message UID.</returns>
        public Hashtable GetUidlList()
        {
            if(!m_Connected){
                throw new Exception("You must connect first !");
            }

            if(!m_Authenticated){
                throw new Exception("You must authenticate first !");
            }

            Hashtable retVal = new Hashtable();

            m_pSocket.WriteLine("UIDL");

            /* NOTE: If reply is +OK, this is multiline respone and is terminated with '.'.
			Examples:
				C: UIDL
				S: +OK
				S: 1 whqtswO00WBw418f9t5JxYwZ
				S: 2 QhdPYR:00WBw1Ph7x7
				S: .
				...
				C: UIDL 3
				S: -ERR no such message
			*/

            // Read first line of reply, check if it's ok
            string line = m_pSocket.ReadLine();
            if(line.StartsWith("+OK")){
                // Read lines while get only '.' on line itshelf.				
                while(true){
                    //long d = m_pSocket.ReadedCount;
                    line = m_pSocket.ReadLine();

                    // End of data
                    if( line.Trim() == "." )
                    {
                        break;
                    }
                    else
                    {
                        string[] param = line.Trim().Split(new char[]{' '});
                        int    nr  = Convert.ToInt32(param[0]);
                        string uid = param[1];

                        retVal.Add(nr,uid);
                    }
                }
            }
            else{
                throw new Exception("Server returned:" + line);
            }

            return retVal;
        }

        #endregion

        #region method GetMessage

        /// <summary>
        /// Gets specified message.
        /// </summary>
        /// <param name="msgInfo">Pop3 message info of message what to get.</param>
        /// <returns></returns>
        public byte[] GetMessage(POP3_MessageInfo msgInfo)
        {
            return GetMessage(msgInfo.MessageNumber);
        }

        /// <summary>
        /// Gets specified message.
        /// </summary>
        /// <param name="messageNo">Message number.</param>
        public byte[] GetMessage(int messageNo)
        {
            MemoryStream stream = new MemoryStream();
            GetMessage(messageNo,stream);
            return stream.ToArray();
        }

        /// <summary>
        /// Gets specified message and stores it to specified stream.
        /// </summary>
        /// <param name="messageNo">Message number.</param>
        /// <param name="stream">Stream where to store message.</param>
        public void GetMessage(int messageNo,Stream stream)
        {
            if(!m_Connected){
                throw new Exception("You must connect first !");
            }

            if(!m_Authenticated){
                throw new Exception("You must authenticate first !");
            }

            m_pSocket.WriteLine("RETR " + messageNo.ToString());

            // Read first line of reply, check if it's ok
            string line = m_pSocket.ReadLine();
            if(line.StartsWith("+OK")){    
                m_pSocket.ReadPeriodTerminated(stream,100000000);
            }
            else{
                throw new Exception("Server returned:" + line);
            }
        }

        #endregion

        #region method DeleteMessage

        /// <summary>
        /// Deletes specified message
        /// </summary>
        /// <param name="messageNr">Message number.</param>
        public void DeleteMessage(int messageNr)
        {
            if(!m_Connected){
                throw new Exception("You must connect first !");
            }

            if(!m_Authenticated){
                throw new Exception("You must authenticate first !");
            }

            m_pSocket.WriteLine("DELE " + messageNr.ToString());

            // Read first line of reply, check if it's ok
            string line = m_pSocket.ReadLine();
            if(!line.StartsWith("+OK")){
                throw new Exception("Server returned:" + line);
            }
        }

        #endregion

        #region method GetTopOfMessage

        /// <summary>
        /// Gets top lines of message.
        /// </summary>
        /// <param name="nr">Message number which top lines to get.</param>
        /// <param name="nLines">Number of lines to get.</param>
        public byte[] GetTopOfMessage(int nr,int nLines)
        {
            if(!m_Connected){
                throw new Exception("You must connect first !");
            }

            if(!m_Authenticated){
                throw new Exception("You must authenticate first !");
            }
			

            m_pSocket.WriteLine("TOP " + nr.ToString() + " " + nLines.ToString());

            // Read first line of reply, check if it's ok
            string line = m_pSocket.ReadLine();
            if(line.StartsWith("+OK")){
                MemoryStream strm = new MemoryStream();
                m_pSocket.ReadPeriodTerminated(strm,100000000);

                return strm.ToArray();
            }
            else{
                throw new Exception("Server returned:" + line);
            }
        }

        #endregion

        #region method Reset

        /// <summary>
        /// Resets session.
        /// </summary>
        public void Reset()
        {
            if(!m_Connected){
                throw new Exception("You must connect first !");
            }

            if(!m_Authenticated){
                throw new Exception("You must authenticate first !");
            }

            m_pSocket.WriteLine("RSET");

            // Read first line of reply, check if it's ok
            string line = m_pSocket.ReadLine();
            if(!line.StartsWith("+OK")){
                throw new Exception("Server returned:" + line);
            }
        }

        #endregion


        #region Properties Implementation

        /// <summary>
        /// Gets if pop3 client is connected.
        /// </summary>
        public bool Connected
        {
            get{ return m_Connected; }
        }

        /// <summary>
        /// Gets if pop3 client is authenticated.
        /// </summary>
        public bool Authenticated
        {
            get{ return m_Authenticated; }
        }

        /// <summary>
        /// Gets or sets if to log commands.
        /// </summary>
        public bool LogCommands
        {
            get{ return m_LogCmds;	}

            set{ m_LogCmds = value; }
        }
        
        /// <summary>
        /// Gets if the connection is an SSL connection.
        /// </summary>
        public bool IsSecureConnection
        {
            get{ 
                if(!m_Connected){
                    throw new Exception("You must connect first");
                }

                return m_pSocket.SSL; 
            }
        }

        #endregion

    }
}